/*
 * Copyright (C) 2021 The Chinese Software International Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.tonyodev.fetch2;

import com.tonyodev.fetch2core.Downloader;
import com.tonyodev.fetch2core.FetchCoreUtils;
import com.tonyodev.fetch2core.InterruptMonitor;

import java.io.InputStream;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static java.util.Collections.emptyList;

/**
 * description HttpUrlConnectionDownloader
 *
 * @author liuluming
 * @since 2021-05-20
 */
public class HttpUrlConnectionDownloader implements Downloader<HttpURLConnection, Void> {

    private final HttpUrlConnectionPreferences connectionPrefs;
    private final Map<Downloader.Response, HttpURLConnection> connections;
    private final CookieManager cookieManager;
    private final FileDownloaderType fileDownloaderType;

    public HttpUrlConnectionDownloader(FileDownloaderType fileDownloaderType) {
        this(null, fileDownloaderType);
    }

    public HttpUrlConnectionDownloader(HttpUrlConnectionPreferences httpUrlConnectionPreferences,
                                       FileDownloaderType fileDownloaderType) {
        this.fileDownloaderType = fileDownloaderType;
        this.connectionPrefs = httpUrlConnectionPreferences == null ?
            new HttpUrlConnectionPreferences() : httpUrlConnectionPreferences;
        this.connections = Collections.synchronizedMap(new HashMap<>());
        this.cookieManager = FetchCoreUtils.getDefaultCookieManager();
    }

    protected HttpUrlConnectionPreferences getConnectionPrefs() {
        return this.connectionPrefs;
    }

    protected Map<Downloader.Response, HttpURLConnection> getConnections() {
        return this.connections;
    }

    protected CookieManager getCookieManager() {
        return this.cookieManager;
    }

    @Override
    public Void onPreClientExecute(HttpURLConnection client, ServerRequest request) {
        try {
            client.setRequestMethod(request.getRequestMethod());
        } catch (ProtocolException e) {
            e.printStackTrace();
        }
        client.setReadTimeout(this.connectionPrefs.getReadTimeout());
        client.setConnectTimeout(this.connectionPrefs.getConnectTimeout());
        client.setUseCaches(this.connectionPrefs.getUsesCache());
        client.setDefaultUseCaches(this.connectionPrefs.getUsesDefaultCache());
        client.setInstanceFollowRedirects(this.connectionPrefs.getFollowsRedirect());
        client.setDoInput(true);
        for (String key : request.getHeaders().keySet()) {
            client.addRequestProperty(key, request.getHeaders().get(key));
        }
        return null;
    }

    /**
     * getCleanedHeaders
     *
     * @param responseHeaders responseHeaders
     * @return true
     */
    private Map<String, List<String>> getCleanedHeaders(Map<String, List<String>> responseHeaders) {
        Map<String, List<String>> headers = new LinkedHashMap<>();
        Iterator<Map.Entry<String, List<String>>> iterator = responseHeaders.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, List<String>> responseHeader = iterator.next();
            String key = responseHeader.getKey();
            if (key != null) {
                List<String> list = responseHeader.getValue();
                if (list == null) {
                    list = emptyList();
                }
                headers.put(key, list);
            }
        }

        return headers;
    }

    /**
     * execute
     *
     * @param request The request information for the download.
     * @param interruptMonitor Notifies the downloader that there may be an interruption for the request.
     * If an interruption occurs, the execute method should return quickly.
     * @return true
     */
    @Override
    public Downloader.Response execute(ServerRequest request, InterruptMonitor interruptMonitor) {
        CookieHandler.setDefault(cookieManager);
        try {
            URL httpUrl = new URL(request.getUrl());
            HttpURLConnection client = (HttpURLConnection) httpUrl.openConnection();
            onPreClientExecute(client, request);
            if (client.getRequestProperty("Referer") == null) {
                String referer = FetchCoreUtils.getRefererFromUrl(request.getUrl());
                client.addRequestProperty("Referer", referer);
            }

            client.connect();
            Map<String, List<String>> map = client.getHeaderFields();
            Map<String, List<String>> responseHeaders = getCleanedHeaders(map);
            int code = client.getResponseCode();
            if ((code == 302 || code == 301 || code == 303) && FetchCoreUtils.getHeaderValue(responseHeaders, "Location") != null) {
                try {
                    client.disconnect();
                } catch (Exception var15) {
                }

                String headerValue = FetchCoreUtils.getHeaderValue(responseHeaders, "Location");
                headerValue = headerValue == null ? "" : headerValue;
                httpUrl = new URL(headerValue);
                client = (HttpURLConnection) httpUrl.openConnection();
                onPreClientExecute(client, request);
                if (client.getRequestProperty("Referer") == null) {
                    String referer = FetchCoreUtils.getRefererFromUrl(request.getUrl());
                    client.addRequestProperty("Referer", referer);
                }
                client.connect();
                responseHeaders = this.getCleanedHeaders(map);
                code = client.getResponseCode();
            }

            boolean success = false;
            long contentLength = -1L;
            InputStream byteStream = null;
            String errorResponseString = null;
            String hash = "";
            if (this.isResponseOk(code)) {
                success = true;
                contentLength = FetchCoreUtils.getContentLengthFromHeader(responseHeaders, contentLength);
                byteStream = client.getInputStream();
                hash = this.getContentHash(responseHeaders);
            } else {
                errorResponseString = FetchCoreUtils.copyStreamToString(client.getErrorStream(), false);
            }

            boolean acceptsRanges = FetchCoreUtils.acceptRanges(code, responseHeaders);
            this.onServerResponse(request, new Response(code, success, contentLength, null, request, hash, map, acceptsRanges, errorResponseString));
            Response response = new Response(code, success, contentLength, byteStream, request, hash, responseHeaders, acceptsRanges, errorResponseString);
            this.connections.put(response, client);
            return response;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    protected final boolean isResponseOk(int responseCode) {
        return 200 <= responseCode && responseCode <= 299;
    }

    /**
     * disconnect
     *
     * @param response Response containing the server response code, headers, connection success, content-length,
     * and input stream if a connection was successful.
     */
    @Override
    public void disconnect(Downloader.Response response) {
        if (connections.containsKey(response)) {
            HttpURLConnection client = connections.get(response);
            connections.remove(response);
            disconnectClient(client);
        }

    }

    @Override
    public Integer getFileSlicingCount(ServerRequest request, Long contentLength) {
        return 0;
    }

    /**
     * getContentHash
     *
     * @param responseHeaders List of headers from response
     * @return true
     */
    @Override
    public String getContentHash(Map<String, List<String>> responseHeaders) {
        String contentHash = FetchCoreUtils.getHeaderValue(responseHeaders, "Content-MD5");
        if (contentHash == null) {
            contentHash = "";
        }
        return contentHash;
    }

    @Override
    public void close() {
        Set<Map.Entry<Response, HttpURLConnection>> iterable = this.connections.entrySet();
        Iterator<Map.Entry<Response, HttpURLConnection>> iterator = iterable.iterator();
        while (iterator.hasNext()) {
            Map.Entry<Response, HttpURLConnection> next = iterator.next();
            disconnectClient(next.getValue());
        }
        connections.clear();
    }

    /**
     * disconnectClient
     *
     * @param client client
     */
    private void disconnectClient(HttpURLConnection client) {
        try {
            if (client != null) {
                client.disconnect();
            }
        } catch (Exception exception) {
        }

    }

    /**
     * getRequestFileDownloaderType
     *
     * @param request the request information for the download.
     * @param supportedFileDownloaderTypes a set of file downloader types supported by the request.
     * @return true
     */
    @Override
    public FileDownloaderType getRequestFileDownloaderType(ServerRequest request, Set<FileDownloaderType> supportedFileDownloaderTypes) {
        return this.fileDownloaderType;
    }

    /**
     * verifyContentHash
     *
     * @param request the request information for the download.
     * @param hash Hash returned by the server for the content
     * @return true
     */
    @Override
    public boolean verifyContentHash(ServerRequest request, String hash) {
        if (hash == null || hash.length() == 0) {
            return true;
        } else {
            String fileHash = FetchCoreUtils.getFileMd5String(request.getFile());
            boolean isFileHash;
            if (fileHash != null) {
                isFileHash = fileHash.contentEquals(hash);
            } else {
                isFileHash = true;
            }

            return isFileHash;
        }
    }

    @Override
    public void onServerResponse(ServerRequest request, Response response) {

    }

    @Override
    public boolean getHeadRequestMethodSupported(ServerRequest request) {
        return false;
    }

    @Override
    public int getRequestBufferSize(ServerRequest request) {
        return 8192;
    }

    @Override
    public long getRequestContentLength(ServerRequest request) {
        return FetchCoreUtils.getRequestContentLength(request, this);
    }

    /**
     * getRequestSupportedFileDownloaderTypes
     *
     * @param request The request information for the download.
     * @return true
     */
    @Override
    public Set<Downloader.FileDownloaderType> getRequestSupportedFileDownloaderTypes(ServerRequest request) {
        LinkedHashSet<Downloader.FileDownloaderType> fileDownloaderTypeSet = new LinkedHashSet<>(1);
        if (this.fileDownloaderType == FileDownloaderType.SEQUENTIAL) {
            fileDownloaderTypeSet.add(fileDownloaderType);
            return fileDownloaderTypeSet;
        } else {
            try {
                return FetchCoreUtils.getRequestSupportedFileDownloaderTypes(request, this);
            } catch (Exception e) {
                fileDownloaderTypeSet.add(fileDownloaderType);
                return fileDownloaderTypeSet;
            }
        }
    }

    public static class HttpUrlConnectionPreferences {
        private int readTimeout = 20000;
        private int connectTimeout = 15000;
        private boolean usesCache;
        private boolean usesDefaultCache;
        private boolean followsRedirect = true;

        public final int getReadTimeout() {
            return this.readTimeout;
        }

        public final void setReadTimeout(int timeout) {
            this.readTimeout = timeout;
        }

        public final int getConnectTimeout() {
            return this.connectTimeout;
        }

        public final void setConnectTimeout(int connectTimeout) {
            this.connectTimeout = connectTimeout;
        }

        public final boolean getUsesCache() {
            return this.usesCache;
        }

        public final void setUsesCache(boolean usesCache) {
            this.usesCache = usesCache;
        }

        public final boolean getUsesDefaultCache() {
            return this.usesDefaultCache;
        }

        public final void setUsesDefaultCache(boolean usesDefaultCache) {
            this.usesDefaultCache = usesDefaultCache;
        }

        public final boolean getFollowsRedirect() {
            return this.followsRedirect;
        }

        public final void setFollowsRedirect(boolean followsRedirect) {
            this.followsRedirect = followsRedirect;
        }
    }
}
